﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;

namespace osuStd2Key
{
    public class osuNoteRecognizeException : Exception
    {
        public osuNote.Note Note;
        public osuNote.Note Note2;

        public osuNoteRecognizeException(string message) : base(message)
        {
        }
        public osuNoteRecognizeException(string message, osuNote.Note note1) : base(message)
        {
            Note = note1;
        }
        public osuNoteRecognizeException(string message, osuNote.Note note1, osuNote.Note note2) : base(message)
        {
            Note = note1;
            Note2 = note2;
        }
        public override string Message
        {
            get
            {
                string message = base.Message;
                if (Note != null) message += " Note1: time=" + Note.TimeMs + ";";
                if (Note2 != null) message += " Note2: time=" + Note2.TimeMs+";";
                return message;
            }
        }
    }
    public class osuStd2KeyConverter : IConverter
    {
        public string FromFile;
        public string ToFile;
        public double BeatPerMs; // 60000/BPM
        public osuStd2KeyConverter(string fromFile, string toFile)
        {
            FromFile = fromFile;
            ToFile = toFile;
        }
        public override void DoConvert() {
            string osuNoteContent = null;
            try
            {
                osuNoteContent = File.ReadAllText(FromFile);
            }
            catch (Exception e)
            {
                LogExc(e, "读取osu谱面文件失败.");
                SetConvertResult(false);
                return;
            }
            osuNote osu = null;
            KeyNote key = new KeyNote();
            try
            {
                osu = new osuNote(osuNoteContent);
                BeatPerMs = osu.getBPM();
                double BPM = 60000 / BeatPerMs;
                Log("info", string.Format("BPM {0}", BPM));
            }
            catch (Exception e)
            {
                LogExc(e, "解析osu谱面失败.");
                SetConvertResult(false);
                return;
            }
            try
            {
                DoConvertLogic(osu, key);
            }
            catch (Exception e)
            {
                LogExc(e, "osu转换为key失败.");
                SetConvertResult(false);
                return;
            }
            try
            {
                File.WriteAllText(ToFile, key.ToString());
            }
            catch (Exception e)
            {
                LogExc(e, "写出key谱面文件失败.");
                SetConvertResult(false);
                return;
            }
            SetConvertResult(true, "转换成功.");
        }

        /*
            osu xy 512*384
            key xy 1136*-639
            单点-单键
            slide-方向
            whistle/finish-长按开始/结束 bpm4*4 长按长度
            大圆盘-showTime


            目前的思想是这样
            1.先把osu的毫秒转换为Key谱面的Frame
            2.按Frame分组 识别出单/双键
            3.判断osuNote对应的Key NoteType
            3.1 根据NoteType进行处理 (Slide转方向 LongNote的bpm4X4时长)
            4.写进Key.Note的数据
         */
        public void DoConvertLogic(osuNote osu, KeyNote key)
        {
            KeyNote.Note kNote;
            KeyNote.Vector2 v2;
            
            // 对应坐标处理双键Hold
            var holdMap = new Dictionary<KeyNote.Vector2, osuNote.Note>();

            for (int i = 0; i < osu.Notes.Count; i++)
            {
                osuNote.Note osuNote = osu.Notes[i];

                switch (osuNote.RealNoteType)
                {
                    case osuStd2Key.osuNote.NoteType.Circle:
                        if (osuNote.isSoundWhistle && !osuNote.isSoundFinish)
                        {
                            //->单键长按开始
                            v2 = osuPosToKeyPos(osuNote.x, osuNote.y);
                            if (holdMap.ContainsKey(v2) && holdMap[v2] != null)
                            {
                                throw new osuNoteRecognizeException("未结束的长按Note1，在同一位置被Note2再次开始.", holdMap[v2], osuNote);
                            }
                            holdMap[v2] = osuNote;
                        }
                        else if (osuNote.isSoundFinish)
                        {
                            //->单键长按结束
                            v2 = osuPosToKeyPos(osuNote.x, osuNote.y);
                            if (!holdMap.ContainsKey(v2)) 
                                throw new osuNoteRecognizeException("未开始的长按被Note结束.", osuNote);

                            // calc the duration of 长按
                            int dTime = osuNote.TimeMs - holdMap[v2].TimeMs;

                            kNote = GenerateKeyNote(
                                holdMap[v2].TimeMs,
                                osuNote.x, osuNote.y,
                                KeyNote.NoteType.Normal_LONG,
                                dTime
                            );
                            key.Notes.Add(kNote);

                            // clear the flag
                            holdMap.Remove(v2);
                        }
                        else
                        {
                            //->单键
                            if (!osuNote.isSoundNormal)
                                Log("Notice", "Time={0}ms时的Circle带有未标记的Sound, 已识别为普通的单键.");
                            key.Notes.Add(GenerateKeyNote(
                                osuNote.TimeMs, 
                                osuNote.x, osuNote.y, 
                                KeyNote.NoteType.Normal));
                        }
                        break;
                    case osuStd2Key.osuNote.NoteType.Slide:
                        //->单键方向

                        string[] multiNoteInfo = osuNote.ExtraInfo.Split(new char[] { '|' });
                        //把第一组节点的坐标取出来
                        string[] nodeInfo = multiNoteInfo[1].Split(new char[] { ':', ',' });
                        int x1 = int.Parse(nodeInfo[0]);
                        int y1 = int.Parse(nodeInfo[1]);

                        KeyNote.WayDirection wayDir = RecognizeWayDirection(osuNote.x, osuNote.y, x1, y1);

                        if (wayDir == KeyNote.WayDirection.unknown)
                        {
                            throw new osuNoteRecognizeException("Misrecognize Slide Direction. (deltaX=0 && deltaY=0)", osuNote);
                        }

                        kNote = GenerateKeyNote(
                                osuNote.TimeMs,
                                osuNote.x, osuNote.y,
                                KeyNote.NoteType.Normal_WAY);
                        kNote.wayAngle = wayDir;
                        key.Notes.Add(kNote);

                        break;
                    case osuStd2Key.osuNote.NoteType.Spinner:
                        //->showTime

                        // calc the duration of ShowTime
                        string strTim = osuNote.ExtraInfo.Split(new char[]{ ',' })[0];
                        int dTim = int.Parse(strTim) - osuNote.TimeMs;

                        kNote = GenerateKeyNote(
                                    osuNote.TimeMs,
                                    0, 0,
                                    KeyNote.NoteType.SHOW_TIME,
                                    dTim
                                );

                        key.Notes.Add(kNote);
                        break;

                    default:
                        Log("Warning", "未处理的类型" + osuNote.RealNoteType.ToString());
                        break;

                }
            } // end for

            // Group Notes by frame
            var noteHandler = new Dictionary<int, List<KeyNote.Note>>();
            foreach (KeyNote.Note note in key.Notes)
            {
                if (!noteHandler.ContainsKey(note.frame))
                    noteHandler[note.frame] = new List<KeyNote.Note>();
                noteHandler[note.frame].Add(note);
            }
            key.Notes.Clear();
            foreach (List<KeyNote.Note> noteGroup in noteHandler.Values)
            {
                RecognizeDoubleNote(noteGroup);
                // Rebuild NoteList
                key.Notes.AddRange(noteGroup);
            }

            // sort the key notes
            key.Notes.Sort(delegate (KeyNote.Note n1, KeyNote.Note n2) {
                return n1.frame.CompareTo(n2.frame);
            });

        }
        
        public KeyNote.Note GenerateKeyNote(int osuMs, int x, int y, KeyNote.NoteType type)
        {
            var kNote = new KeyNote.Note()
            {
                frame = osuTimeMsToKeyFrame(osuMs),
                type = type
            };
            osuPosToKeyPos(x, y).ToNotePos(kNote);
            return kNote;
        }
        public KeyNote.Note GenerateKeyNote(int osuMs, int x, int y, KeyNote.NoteType type, int deltaMs)
        {
            var kNote = GenerateKeyNote(osuMs, x, y, type);
            kNote.bpm4X4 = osuTimeMsToKeyFrame(deltaMs);
            return kNote;
        }
        public int osuTimeMsToKeyFrame(int osuTimeMs)
        {
            return (int)Math.Round(osuTimeMs / BeatPerMs * 4);
        }
        public int KeyFrame2osuTimeMs(int keyFrame)
        {
            return (int)Math.Round(keyFrame * BeatPerMs / 4);
        }
        public KeyNote.Vector2 osuPosToKeyPos(int x, int y)
        {
            // osu Pos Range (0,0) -> (512, 384)
            // Key Pos Range (0,0) -> (1136, -639)

            int dstX = (int)Math.Round(x / 512.0 * 1136.0);
            int dstY = (int)Math.Round(y / 384.0 * -639.0);

            return new KeyNote.Vector2(dstX, dstY);
        }

        public void RecognizeDoubleNote(List<KeyNote.Note> notesAtTime)
        {
            List<KeyNote.Note> newNoteGroup = new List<KeyNote.Note>();
            KeyNote.Note firstNote = null;
            bool Doubled = false;
            foreach (KeyNote.Note n in notesAtTime)
            {
                if (n.type == KeyNote.NoteType.SHOW_TIME || n.type == KeyNote.NoteType.HEART)
                {
                    newNoteGroup.Add(n);
                    continue;
                }
                if (Doubled)
                    throw new osuNoteRecognizeException(string.Format("Time={0}ms 时已经有一个双键，但出现了第三个Note.", KeyFrame2osuTimeMs(n.frame)));
                if (firstNote == null) // FirstNote
                {
                    firstNote = n;
                    newNoteGroup.Add(n);
                    continue;
                }
                else if (firstNote.type != n.type)
                    throw new osuNoteRecognizeException(string.Format("Time={0}ms 时有两个不兼容的键类型{1},{2}", KeyFrame2osuTimeMs(n.frame), firstNote.type, n.type));
                Doubled = true;
                switch (firstNote.type)
                {
                    case KeyNote.NoteType.Normal:
                        firstNote.type = KeyNote.NoteType.DOUBLE;
                        break;
                    case KeyNote.NoteType.Normal_LONG:
                        firstNote.type = KeyNote.NoteType.DOUBLE_LONG;
                        if (firstNote.bpm4X4 != n.bpm4X4)
                            throw new osuNoteRecognizeException(string.Format("Time={0}ms 时双键长按的时长不相同 {1}!={2}", KeyFrame2osuTimeMs(n.frame), firstNote.bpm4X4, n.bpm4X4));
                        break;
                    case KeyNote.NoteType.Normal_WAY:
                        firstNote.type = KeyNote.NoteType.DOUBLE_WAY;
                        if (firstNote.wayAngle != n.wayAngle)
                            throw new osuNoteRecognizeException(string.Format("Time={0}ms 时双键方向的方向不相同 {1}!={2}", KeyFrame2osuTimeMs(n.frame), firstNote.wayAngle, n.wayAngle));
                        break;
                    default:
                        throw new osuNoteRecognizeException(string.Format("Unsupported KeyNoteType:{0} for double note.", firstNote.type));
                }
                firstNote.x1 = n.x - firstNote.x;
                firstNote.y1 = n.y - firstNote.y;
            }
            notesAtTime.Clear();
            notesAtTime.AddRange(newNoteGroup);
        }
        public KeyNote.WayDirection RecognizeWayDirection(int x, int y, int x1, int y1)
        {
            // Slide方向转为Key的八方向
            // 用那个九宫格直线矩形相交的位运算会更漂亮吧.. what the hell

            KeyNote.WayDirection ret = KeyNote.WayDirection.unknown;

            int dx = x1 - x;
            int dy = y - y1;
            //0 ~ -639 故反转deltaY符号

            if (dx < 0)
            {
                if (dy == 0) ret = KeyNote.WayDirection.Left;
                else if (dy < 0) ret = KeyNote.WayDirection.DownLeft;
                else ret = KeyNote.WayDirection.UpLeft;
            }
            else if (dx > 0)
            {
                if (dy == 0) ret = KeyNote.WayDirection.Right;
                else if (dy < 0) ret = KeyNote.WayDirection.DownRight;
                else ret = KeyNote.WayDirection.UpRight;
            }
            else
            {
                if (dy == 0) ret = KeyNote.WayDirection.unknown;
                else if (dy < 0) ret = KeyNote.WayDirection.Down;
                else ret = KeyNote.WayDirection.Up;
            }

            return ret;
        }
    }
}
